package com.Jano.util.jwt;

import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;

import javax.xml.bind.DatatypeConverter;
import java.time.Duration;
import java.util.Date;
import java.util.Map;

/**
 * @ClassName JwtTokenUtil
 * @Description
 * JWT生成token：根据官方文档==> 载荷为 JWT 规范中定义的JwtBuilder标准注册声明名称提供了方便的 setter 方法 那么有以下set方法 都是可选的
 * setIssuer  发行人                                         使用  配置文件直接定义
 * setSubject  主题 一般为userId                             使用  使用时传入
 * setExpiration  过期时间                                   使用  配置文件直接定义
 * setIssuedAt：  发行时间                                   使用  配置文件直接定义
 * setNotBefore   声明标识了 JWT 之前的时间    不得接受处理     未使用
 * setId          声明为 JWT 提供了唯一标识符                  未使用
 * 我们需要设置一个或多个与上面显示的标准 setter 方法声明不匹配的自定义声明，可以直接放在chain中
 * 一般我们可以放入一些用户的权限,用户的名字等等
 *
 * 此工具类提供获取用户id即getSubject 但是存入的时候需要存userId
 * 此工具类提供获取用户名userName,但是需要存入chain 以自定义键名以及值
 *
 * 校验令牌：
 * 我们的封装思路如下:
 * 首先我们利用签名密钥解析器JWR提供的
 * @Author Administrator
 * @Date: 2022/7/14 14:18
 * @Version 1.0
 */
public class JwtTokenUtil {
    private static String secretKey;
    private static Duration accessTokenExpireTime;
    private static Duration refreshTokenExpireTime;
    private static Duration refreshTokenExpireAppTime;
    private static String issuer;

    public static void setTokenSettings( TokenSettings tokenSettings) {
        //密钥
        secretKey = tokenSettings.getSecretKey();
        //访问令牌过期时间
        accessTokenExpireTime = tokenSettings.getAccessTokenExpireTime();
        //刷新访问令牌过期时间(PC)
        refreshTokenExpireTime = tokenSettings.getRefreshTokenExpireTime();
        //刷新访问令牌过期时间(App)
        refreshTokenExpireAppTime = tokenSettings.getRefreshTokenExpireAppTime();
        //发行人
        issuer = tokenSettings.getIssuer();
    }

    /**
     * 生成 access_token 访问令牌过期时间
     * 用于用户登录成功后,向用户签发令牌
     *
     * @param subject 代表这个JWT的主题，即它的所有人 一般是用户id
     * @param claims  存储在JWT里面的信息 一般放些用户的权限/角色信息
     * @return
     */
    public static String getAccessToken(String subject, Map<String, Object> claims) {
        return generateToken(issuer, subject, claims, accessTokenExpireTime.toMillis(), secretKey);
    }

    /**
     * 生产 App端 refresh_token 刷新访问令牌过期时间(PC)
     *
     * @param subject 代表这个JWT的主体，即它的所有人 一般是用户id
     * @claims 存储在JWT里面的信息 一般放些用户的权限/角色信息
     */
    public static String getRefreshAppToken(String subject, Map<String, Object> claims) {
        return generateToken(issuer, subject, claims, refreshTokenExpireAppTime.toMillis(), secretKey);
    }

    /**
     * 生产 PC refresh_token 刷新访问令牌过期时间(App)
     *
     * @param subject 代表这个JWT的主体，即它的所有人 一般是用户id
     * @claims 存储在JWT里面的信息 一般放些用户的权限/角色信息
     */

    public static String getRefreshToken(String subject, Map<String, Object> claims) {
        return generateToken(issuer, subject, claims, refreshTokenExpireTime.toMillis(), secretKey);
    }

    /**
     * 签发token
     * @param issuer    签发人
     * @param subject   代表这个JWT的主题，即它的所有人 一般是用户id
     * @param claims    存储在JWT里面的信息 一般放些用户的权限/角色信息
     * @param ttlMillis 有效时间(毫秒)
     * @param secret    密钥
     * @return java.lang.String
     * @throws
     */
    public static String generateToken(String issuer, String subject, Map<String, Object> claims, long ttlMillis, String secret) {
        //获取当前系统时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        //DatatypeConverter：
        // 在类里面提供了一些静态方法，这些方法就是Decode（解码）或者Encode(编码)使用，提供一些Java中数据格式的转换
        //简单来说就是各种常见数据类型之间的相互转化，
        // 如int long string byte hex decimal base64 unsigned date等相互转化
        // 就是将字节数组做base64编码
        // 这里不直接转换为字节数组的原因是二次加密更加安全,因为HS256还是有一定几率破解的,再加密一次机率几乎为0
        byte[] signingKey = DatatypeConverter.parseBase64Binary(secret);

        JwtBuilder builder = Jwts.builder();
        if (null != claims) {
            //claims：存储在JWT里面的信息 一般放些用户的权限/角色信息
            builder.setClaims(claims);
        }
        if (!StringUtils.isEmpty(subject)) {
            //subject: 代表这个JWT的主体，即它的所有人 一般是用户id
            builder.setSubject(subject);
        }
        if (!StringUtils.isEmpty(issuer)) {
            // issuer发行人
            builder.setIssuer(issuer);
        }
        //发行时间
        builder.setIssuedAt(now);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            //到期的时间
            builder.setExpiration(exp);
        }
        //签名算法 签发token所用的算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //使用指定的算法和指定的密钥对构造的 JWT 进行签名，生成 JWS
        builder.signWith(signatureAlgorithm, signingKey);
        return builder.compact();
    }

    /**
     * 获取用户id
     */
    public static String getUserId(String token) {
        String userId = null;
        try {
            Claims claims = getClaimsFromToken(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            //log.error("eror={}",e);
            e.printStackTrace();
        }
        return userId;
    }

    /**
     * 获取用户名
     */
    public static String getUserName(String token) {

        String username = null;
        try {
            Claims claims = getClaimsFromToken(token);
            username = (String) claims.get(Constant.JWT_USER_NAME);
        } catch (Exception e) {
            //log.error("eror={}",e);
            e.printStackTrace();
        }
        return username;
    }

    /**
     * 从令牌中获取数据声明
     */
    public static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(secretKey)).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
            logger.error("当前从令牌中获取数据声明的bug==>"+e);
            claims = null;
        }
        return claims;
    }

    /**
     * 校验令牌
     */
    public static Boolean validateToken(String token) {
        Claims claimsFromToken = getClaimsFromToken(token);
        //获取数据是否为空&&token是否过期
        //TODO ？？
        return (null != claimsFromToken && !isTokenExpired(token));
    }

    /**
     * 验证token 是否过期
     */
    public static Boolean isTokenExpired(String token) {

        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            //log.error("error={}",e);
            e.printStackTrace();
            return true;
        }
    }

    /**
     * 刷新token
     * 如用户信息进行改变，则token中用户信息也应该刷新，则需刷新token，
     * 把最新的用户信息放入token中，且过期时间重新计算
     */
    public static String refreshToken(String refreshToken, Map<String, Object> claims) {
        String refreshedToken;
        try {
            Claims parserClaims = getClaimsFromToken(refreshToken);
            /**
             * 刷新token的时候如果为空说明原先的用户信息不变 所以就引用上个token里的内容
             */
            if (null == claims) {
                claims = parserClaims;
            }
            refreshedToken = generateToken(parserClaims.getIssuer(), parserClaims.getSubject(), claims, accessTokenExpireTime.toMillis(), secretKey);
        } catch (Exception e) {
            refreshedToken = null;
            //log.error("error={}",e);
            e.printStackTrace();
        }
        return refreshedToken;
    }

    /**
     * 获取token的剩余过期时间
     */
    public static long getRemainingTime(String token) {
        long result = 0;
        try {
            long nowMillis = System.currentTimeMillis();
            result = getClaimsFromToken(token).getExpiration().getTime() - nowMillis;
        } catch (Exception e) {
            //log.error("error={}",e);
            e.printStackTrace();
        }
        return result;
    }
}
